在将基准测试集成到测试框架后,每当应用程序可以在可控环境下稳定运行,或因修改算法实现打了一个小版本时,就应该执行回归测试,以便可以保证将来即使修改了代码,也不会出现性能下降的情况。
执行基准测试的另一个目的是提供一个小的、自包含的沙箱(sandbox),在这个沙箱中针对应用程序的某一部分进行性能调优,以期可以达到软件项目预定的性能目标。
在某些案例中,基准测试的结果表明,应用程序某些模块需要彻底重写,以便使用执行效率更高的算法,而在另一些案例中,只需要调整JVM的参数即可满足要求。
在之前章节中已经提到过,自适应运行时的反馈信息对JVM优化有着至关重要的作用。
理想情况下,使用了自适应运行时的系统应该是根本不需要调优的,自适应运行时会反馈反馈信息适时的调节应用程序的行为。但可惜的是,机器的推断能力还没有强大到可以和人相比的地步,尽管它在查找热点方法和膨胀/收缩锁等方面比人工操作强,但在其他一些方面就力不从心了,譬如说,如果应用程序可用的堆够大的话,根本就不需要缩放堆内存,又或者如果不关心内存碎片问题,那么就不需要执行堆整理的操作等等,自适应运行时无法推测出这些内容,需要由开发人员专门对JVM进行配置。另一方面,如果开发人员高估了自己的水平或者没有掌握足够的信息的话,就可能会做出错误的配置。
应用程序的行为应该通过专门的分析器来收集,就JRockit来说,它使用了非侵入性的行为收集器,可以以较小的性能损耗来记录应用程序的行为,为避免影响应用程序的运行,分析工作可以离线进行。
在调优应用程序之前,首先要明确应用程序的性能瓶颈在哪里,花大力气去优化非瓶颈的代码是不值得的,而且还会增加代码的复杂度。例如,通过JRockit Flight Recorder套件发现应用程序的性能瓶颈是网络处理能力不足,这种情况下,匆忙将10几行的代码换成百十来行的"高端算法"是很不明智的。为了避免引入不必要的复杂性,面对问题时应优先选择简便的解决方案。
在某些案例中,在做完应用程序分析后发现根本不需要对修改应用程序,从分析结果看,调整运行时参数即可解决问题。在介绍相关命令行参数之前,需要强调的一点是,调优所涉及到的很多参数和JVM的行为都是各大JVM厂商非规范性的、自定义的(out of the box,译者注,后面简称为OOTB)。就JRockit来说,每个新版本都会对这些OOTB特性进行大量优化,以便更好的满足用户对应用程序执行性能的要求,使用户可以专注于自身的业务逻辑开发。
最后强调一下,通过命令行参数来自行配置JVM的运行行为可能会产生意料之外的结果,此外,同一款JVM的不同版本对同一命令行参数的支持可能也不尽相同,使用应多加小心。在以JRockit为例,命令行参数以
-XX开头表明该参数的含义代可能会在不同的版本之间发生变化,使用时要注意查看相关文档的说明。
正如在第3章中提到的,无论JVM中所运行的是何种应用程序,调优工作都是一样的,调优的基本目标大致可分为高吞吐、低延迟或近实时,其中,近实时可算作是低延迟的高级特例。
在前几章中已经介绍过这几个优化目标,本节将对其中所涉及到的一些命令行参数做简单介绍,相关内容只针对JRockit进行讲解,其他JVM实现内部原理超出本书范围,此处不再赘述。尽管不同JVM版本对同一命令行参数定义可能有所区别,但其所涉及到的基本原理和技术却是相同的。
在使用命令行参数之前,一定要先查询JRockit文档,尤其是JRockit诊断指南,明确所要使用的命令行参数的具体作用,此外可以通过JRockit Mission Control套件记录使用该参数前后应用程序的行为来加深对该参数的理解。
为了避免把篇幅搞的太大,本节中并没有提供太多的相关示例,这些内容可以在JRockit文档,尤其是JRockit诊断指南中找到。
本节中所介绍的命令行参数均以JRockit R28版本为基准,在其他的版本中,参数的含义可能不尽相同,具体情况请查阅相关文档。
本节将对与内存管理系统和垃圾回收器相关的命令行参数进行介绍。
在第3章中曾经介绍过,命令行参数-Xms和-Xmx分别可以用来设置堆的初始大小的最大容量。
如果应用程序有实时性的要求,则通常会将-Xmx和-Xms的值均设置为当前系统所能支持的最大值,以避免堆在应用程序运行过程中出现伸缩的情况。
示例:
java -Xms1024M -Xmx1024M Application
将堆的初始值和最大值均设置为1GB
针对应用程序的特点选择合适的垃圾回收算法是很重要的。如果对应用程序的响应时间有要求的话,那么不要忘了使用-XpauseTarget参数来设置相应的服务级别。
如果应用程序执行的是批处理作业,优化目标一般是最大化吞吐量,那么就可以设置参数–XgcPrio:throughput.
示例:
java –XgcPrio:pausetime -XpauseTarget:250ms
将JVM的优化目标设置为低延迟,并设定期望的最大暂停时间为250ms
随着应用程序的不断运行,堆中的内存会呈现出碎片化的趋势。早期的应用程序对碎片化没什么办法,只能重启应用程序,但这种方式会加大应用程序的整体延迟,浪费CPU资源。从已有的经验看,对堆的一部分空间进行整理可以有效应对碎片化的问题。JRockit JVM中使用的就是这种策略,并在自适应运行时的帮助下,自行对垃圾回收的行为进行调整。
垃圾回收的一大瓶颈就是内存整理,因为这一步操作很难以完全并发的方式执行。如果能够获得有关内存碎片和对象大小相关的信息(例如可以通过JRockit Flight Recorder套件得到),那么在做调优的时候就能更具针对性。就JRockit来说,可以使用命令行参数-XXcompaction及其相关参数来指定与内存整理相关的行为。
JRockit中内存整理的算法是将堆划分成多个同样大小的分区,内存整理以分区为单位进行,执行时可能需要暂停应用程序。默认配置下,会使用4096个分区,实际应用时如果执行内存整理的速度跟不上内存碎片化的速度,则需要减少分区的数量;如果内存整理是破坏性的,则可以增大分区的数量。典型情况下,如果优化策略不是针对最大化吞吐量的话,垃圾回收器所整理出的内存空间的大小很大程度上取决于执行内存整理的频率。命令行参数–XXcompaction:heapParts可以用来设定所使用的分区数量。
在JRockit中,内存整理分为 内部整理(internal compaction)和外部整理(external compaction)两类,其中外部整理也被称为。内部整理的操作只集中于某个内存分区内部,将对象移动到分区头部,而不会将对象移动到其他分区。外部整理会同时作用于多个分区,并尽量将对象移动到整个堆的头部,从而降低整个堆的碎片化程度。因此,相比于内部整理,外部整理的并发性较低,而且会有一个较长时间的、STW式的操作过程。
内存整理是以滑动窗口的形式完成对整个堆的整理。目前JRockit中会交错使用内部整理和外部整理,如果本次垃圾回收使用的是内部整理,则下一次会使用外部整理。
命令行参数-XXcompcation:internalPercentage和-XXcompcation:externalPercentage分别来用设置执行内存整理时会对多少个分区执行整理操作。
译者注:虽然参数名表明设置的是比例,但实际上设置的是分区数量,参见文档http://docs.oracle.com/cd/E15289_01/doc.40/e15062/optionxx.htm的说明。
如果已知应用程序的对象分配策略,并且期望降低系统延迟,那么可以使用命令行参数–XXcompaction:enable=false来关闭所有的内存整理操作。在启用这个参数之前,应该先通过JRockit Mission Control来确认是否有必要处理碎片化问题。关闭内存整理可以大幅减少内存管理中对暂停Java应用程序的需求,但对于那些内存较大、运行时间较长的应用程序来说,关闭内存整理很有可能最终会使应用程序因发生OutOfMemoryError错误而崩溃。
另一方面,如果应用程序对延迟没什么要求,即使应用程序有较长的暂停时间也不在乎,而是只关心吞吐量的话,那么可以指定–XXcompaction:full参数,该参数会强制垃圾回收器在每次执行垃圾回收时对整个堆做内存整理,这样可以尽可能降低内存中碎片化程度。在某些案例中,对整个堆执行内存整理的速度很慢,导致应用程序暂停时间过长,结果反而使吞吐量下降,因此使用时要仔细分析应用程序的行为特点。
在JRockit Mission Control中,有时也把对整个堆做内存整理的操作称为 异常整理(exceptional compaction)。
对于那些追求低延迟的应用程序来说,如果内存整理的时间过长,则应该终止当前的内存整理操作。在默认的吞吐量优先垃圾回收器中,中断内存整理默认是被禁用的,可以通过设置命令行参数–XXcompaction:abortable来强制启用。
还有一些其他参数可用来对内存整理的操作进行设置,这些内容超出本书范围,此处不再赘述,相关内容请查阅JRockit诊断指南。最后强调一下,当为了提升应用程序的实时性而调整与内存整理相关的参数时,有可能会使应用程序的整体性能有较大偏差,降低对应用程序暂停时间的准确性。
示例:
java –XXcompaction:enable=false <application>
禁用内存整理
java –XXcompaction:full <application>
对整个堆执行内存整理,最大化吞吐量
java –XXcompaction:internalPercentage=1.5, externalPercentage=2,heapParts=512 <application>
将堆划分为512个分区,每次内部整理会处理1.5个分区,每次外部整理会处理2个分区
java –XgcPrio:throughput –XXcompaction:abortable=true <application>
以最大化吞吐量为主要优化目标,同时允许中断内存整理操作以兼顾对低延迟的需求
System.gc()方法命令行参数-XX:AllowSystemGC可用来设置是否允许使用System.gc()方法,设置命令行参数–XX:AllowSystemGC=false会将System.gc()方法变为空方法。默认情况下是允许开发人员调用System.gc()方法的,而调用该方法有可能会引起对整个堆的垃圾回收操作,因此频繁调用该方法反而可能会降低应用程序的整体性能。与System.gc()方法相关的问题及解决方案请参见第3章 自适应内存管理和本章5.7节中的介绍。
另一方面,对于追求高吞吐量的应用程序来说,可以使用命令行参数-XX:FullSystemGC可以强制JVM在调用System.gc()方法时对整个堆执行垃圾回收操作。使用该参数时,一定要小心。
示例:
java –XX:FullSystemGC=true <application>
强制JVM在调用`System.gc()`方法时对整个堆执行垃圾回收操作
在3.3.4节中曾经介绍过,新生代用来存储短生命周期的对象,JVM会根据运行时反馈信息动态调整新生代的大小。如果JVM使用分代式垃圾回收,并且应用程序在运行过程中会产生大量的临时的对象,则可以通过命令行参数-Xns来显示设置一个较大的新生代。当以吞吐量为优先优化目标时 ,可以直接跳过分代式垃圾回收的配置,直接使用命令行参数-XgcPrio:throughput。
示例:
java –Xns:10M <application>
将新生代的大小设置为10M
如果自适应垃圾回收策略由于某些原因而变换得过于频繁,则可以通过命令行参数–XXdisableGCHeuristics来禁用自适应运行时对垃圾回收策略的切换,此时,内存整理和新生代的大小并不受影响。
注意,这个参数只在JRockit R28之前的版本中有效。就R28版本来说,垃圾回收垃圾回收策略的切换操作本来就已经禁用了,因此这个参数也就被废弃了。
在3.4.1节中曾经介绍过,当TLA已满时,会将其中的对象提升到堆中。使用命令行参数–XXtlaSize可以显式的设置TLA区域的大小。当待分配对象的大小超过了TLA剩余容量时,或者在TLA中分配内存会导致过多的空间被浪费掉时,JVM可能会直接在堆中为对象分配内存。这种实现机制可以避免TLA被过快的填满,进而避免了频繁提升对象到堆中的性能损耗。
对于应用程序来说,对大对象的处理确实是个问题。如果已知应用程序中常用对象的大小,则对TLA进行相关设置可以提升执行性能。
示例:
java –XXtlaSize:min=2k,preferred=8k <application>
将TLA理想空间设置为8KB,最小可接受空间为2KB
在JRockit R28之前的版本中,并没有启用TLA,大对象是直接分配在堆中的。命令行参数
–XXlargeObjectLimit用来指定所谓 "大对象"所占空间的最小值,默认值是2KB。从R28版本起,JRockit使用了更为灵活的 浪费限额(waste limit)来替代参数–XXlargeObjectLimit,空闲限制指定了当在TLA中分配大对象时,可以被浪费掉空间的最大值。因此,从R28版本起,在TLA中分配内存时,如果TLA的空闲空间中放不下对象,而且浪费限额的值小于TLA中剩余空闲空间的大小,则直接在堆中为对象分配内存;否则,JRockit会根绝目标对象的大小来决定是 "浪费"掉这个TLA区域,尝试将在一个新的TLA中为对象分配内存,还是直接在堆中为对象分配内存。
示例:
java –XXlargeObjectLimit:16k <application>
将大对象的大小限制提高到16KB,该参数只在JRockit R28之前的版本中有效
java –XXtlaSize:min=16k,preferred=256k,wasteLimit=8k <application>
设置TLA的大小为256KB,最小可收缩到16KB,可接受的最大浪费空间是8KB
更多与大对象相关的优化细节请参见5.7节中的内容。频繁地将还有不少空闲空间的TLA中的对象提升到堆中是很浪费的,而且会抵消掉使用TLA所带来的好处,因此开发人员需要在堆碎片化和频繁创建TLA所带来的性能损耗之间做权衡。
JRockit倾向于假设自己会独占计算机资源,因此会使用尽可能多的线程来执行垃圾回收,直到达到操作系统和物理硬件的限制。典型情况下,JRockit使用的垃圾回收线程数与物理机器的CPU核数相同。如果出于某些原因而不便于这样做,例如还有其他应用程序也需要大量使用CPU,则可以通过命令行参数–XXgcThreads来显式指定垃圾回收的线程数。
如果垃圾回收线程数太少,则可能会导致垃圾回收的速度跟不上垃圾对象的生产速度,在极端情况下,会导致JVM抛出OutOfMemoryError错误,不过更有可能导致JVM频繁对整个堆执行垃圾回收,而这将大大增加应用程序的延迟。
示例:
java –XXgcThreads:4 <application>
设置垃圾回收的线程数为4
大部分现代操作系统都可以为进程设置CPU亲和性,使进程始终运行在某个或某几个CPU上。在NUMA架构下,CPU亲和性更显重要,可以通过将JVM进程绑定在某几个NUMA节点上来提高局部性,当然,这样会使动态性和内存访问效率有所降低。因此,在调整CPU亲和性之前,一定要清楚掌握应用程序的行为。
命令行参数–XX:BindToCPUs可强制JRockit只使用某几个固定的CPU核心。
示例:
java –XX:BindToCPUs=0,2 <application>
使JRockit只适用编号为0和2的CPU核心
就NUMA架构来说,还可以使用命令行参数-XX:BindToNumaNodes来控制内存分配策略,该参数用于指定JRockit是在所有的NUMA节点中平均分配内存页,还是只在本地节点中分配内存,参数值preferredlocal表示JRockit会尽可能使用本地节点,参数值interleave表示JRockit会在所有的NUMA节点中平均分配内存。
示例:
java –XX:NumaMemoryPolicy=strictlocal <application>
强制在分配内存时使用本地节点。其他可选值是preferredlocal或interleave。
本节将对与代码生成相关的命令行参数进行介绍。
使用命令行参数-XX:UseCallProfiling,可以让JRockit代码生成器在做即时编译时,添加相应的分析代码来收集代码运行时的相关数据,以便更准确的制定代码优化策略。
当然,为了不影响目标方法的执行性能,不能在做即时编译时随意添加分析代码。但如果应用程序将会运行很长一段时间的话,那么热点方法基本上都会被JIT优化编译器处理过,之前遗留的分析代码也会被优化去掉,如果优化编译器收集到了足够的调用信息,那么经过优化的方法可能会比以普通方法优化的方法运行得更快。
默认情况下,调用分析是被禁用的,不过在将来可能会改为默认启用。该参数尤其适用于那些具有有很长调用链的应用程序。
示例:
java –XX:UseCallProfiling=true <application>
启用调用分析来收集热点方法的调用信息
JRockit中的代码优化算是相关激进的操作,需要耗费大量的CPU时间和内存资源,但如果消耗的太多的CPU资源就不值得了。在2.7.1.1.3节中曾经介绍过,可以通过相关的命令行参数来控制JIT编译器工作时所使用的线程数。如果CPU的工作负载不高,而且物理机器上存在有大量的CPU核心,则调大执行优化编译工作的线程数可以使应用程序更快的达到稳定运行的状态。
JRockit中,命令行参数-XX:OptThreads和-XX:JITThreads分别用来设置优化编译器和即时编译器的工作线程的数量。
这两个参数的默认值都是1,如果想使用更多线程的话,就需要做好相关的基准测试。事实上,即使是只使用1个线程,应用程序最终也能达到稳定运行的状态,只不过所需的时间更长一些。
示例:
java –XX:JITThreads=2 <application>
设置JIT编译器使用2个线程,默认值是1
java –XX:OptThreads=2 <application>
设置优化编译器使用2个线程,默认值是1
代码优化是计算密集型操作,需要耗费很多CPU资源,因此可能会产生过大的执行消耗,例如可能会使热身周期过长,或者使延迟增大,在这种情况下,可以通过命令行参数–XnoOpt来全面禁用代码优化,禁用代码优化之后,JVM的行为将更具可预测性,但执行性能将受到影响,针对于此,可以使用命令行参数-XX:DisableOptsAfter来指定在多少秒之后禁用代码优化操作。
示例:
java –XnoOpt <application>
彻底禁用代码优化
java –XX:DisableOptsAfter=600
十分钟之后禁用代码优化
通常情况下是不需要对锁进行调优的,默认行为就挺不错的,用户自行修改锁的行为通常不会带来什么性能提升,好好调优应用程序才是正经。不过为了保证内容的完整性,本节还是会对JRockit中与控制锁行为相关的命令行参数做简单介绍。
在4.4.4节曾经介绍过,当某个锁对象频繁地被同一个线程加锁/释放时,启用延迟解锁是可以提升整体性能的。如果某个线程会在很短的时间内重新获取某个已经获取到的锁,那么实在没必要为了这么一小段时间而执行锁的释放操作。
在JRockit中,延迟解锁是默认启用的(除非是在Java 1.6版本之前,以准确式垃圾回收运行JRockit),当然,用户也可以通过命令行参数-XX:UseLazyUnlocking来显式指定是否启用延迟解锁。
示例:
java –XX:UseLazyUnlocking=false <application>
显式禁用延迟解锁
此外,当自适应运行时发现,对于某个类的对象来说,延迟解锁的前提假设总是不成立,则会禁止对该类的所有对象做延迟解锁处理。用户可通过命令行参数-XX:UseLazyUnlockingClassBan来显式设置。
java.lang.Thread类支持对线程优先级的设置,但通常虚拟机不会提供具体的实现,因而不会有实际作用。这么做是因为用户自定义线程优先级往往得不偿失,在Java层面设置线程优先级会扰乱操作系统的线程调度策略,可能会引发意想不到的问题。
默认情况下,JRockit会忽略对java.lang.Thread#setPriority(int)方法的调用,用户可以通过命令行参数-XX:UseThreadPriorities来强制其支持对线程优先级的修改操作。
示例:
java –XX:UseThreadPriorities=true <application>
启用对线程优先级的修改功能
高端用户可能会发现,调整锁膨胀或锁收缩的阈值(即何时从瘦锁升级为胖锁,或从胖锁降级为瘦锁),可以提升应用程序的整体性能
示例:
java –XX:ThinLockConvertToFatThreshold=100 <application>
在将瘦锁提升为胖锁之前需要在瘦锁中自旋100次
在JRockit中,自旋操作并不是简单的浪费CPU资源,因为在每次迭代中都会有短时间的暂停或主动让出当前CPU的使用权,所以,在不同的CPU平台上,指定相同的循环迭代次数完全可能会导致不同的总体循环时间。
示例:
java –XX:UseFatLockDeflation=false <application>
禁用锁收缩操作,默认为true
java –XX:FatLockDeflationThreshold=10 <application>
在遇到10次非竞争的锁获取操作之后,执行锁收缩操作
在4.4.1节中曾经提到过,JRockit中的胖锁实现中使用了自旋周期很短的自旋锁,以便在真正进入胖锁之前还能有一次机会通过瘦锁完成操作。这部分行为可以通过命令行参数来修改。
示例:
java –XX:UseFatSpin=false <application>
在胖锁中禁用自旋锁
java –XX:UseAdaptiveFatSpin=false <application>
禁用胖锁中自旋锁的自适应调整
除了上面提到的命令行参数外,还有一些其他的参数可用于控制锁的行为,更多详细内容请查阅JRockit诊断指南。总的来说,在通过命令行参数调整锁的行为时,为避免出现意外情况,一定要谨慎。
还有其他一些与调优相关的命令行参数没有专门的归类,本节将对此做简单介绍。
在3.8.2节中曾经介绍过,在64位平台上,大部分情况下是默认启用引用压缩的,因此不需要显式配置。默认情况下,JRockit会根据当前JVM进程中堆的最大值来选择对应的引用压缩方式,不过用户也可以显式指定具体的压缩方式,使用相关参数之前请查阅相关文档。
示例:
java -XXcompressedRefs:enable=false <application>
禁用引用压缩
java –XXcompressedRefs:enable=true,size=64GB <application>
启用引用压缩,所支持的堆最多可达64GB
译者注,引用压缩对不同大小堆的支持参见这篇文章,https://blogs.oracle.com/jrockit/entry/understanding_compressed_refer
大内存页可应用于代码缓冲区和堆,通过命令行参数-XX:UseLargePagesForHeap和-XX:UseLargePagesForCode来显式指定是否启用,默认情况下,是完全不启用大内存页的。
如果当前操作系统支持大内存页,而且能够合理配置的话,则启用大内存页可以大幅提升TLB的命中率。对于内存密集型应用程序来说,使用大内存页可以使整体性能提升10%~15%。
对于需要长时间运行的大型应用程序来说,推荐试用大内存页支持,如果操作系统不支持大内存页的话,JRockit会打印警告信息,并回归到普通模式运行。
示例:
java –XX:UseLargePagesForCode=true <application>
在代码缓冲区中启用大内存页
java –XX:UseLargePagesForHeap=true <application>
在堆中启用大内存页